﻿/*
 * This file is part of MonoSettlers.
 *
 * Copyright (C) 2010-2011 Christoph Husse
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: 
 *      # Christoph Husse
 * 
 * Also checkout our homepage: http://opensettlers.codeplex.com/
 */
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.IO;
using System.ComponentModel;
using System.Runtime.Serialization;

namespace MonoSettlers
{
    public enum LibraryMode
    {
        Filesystem,
        Assembly,
    }

    public class AnimationLibrary
    {
        private static List<AnimationLibrary> m_Libraries = new List<AnimationLibrary>();
        public static ReadOnlyCollection<AnimationLibrary> Libraries { get { return m_Libraries.AsReadOnly(); } }
        // contains the most recently loaded library reference
        public static AnimationLibrary Instance { get; private set; }

        private UniqueMap<String, AnimationClass> m_Classes;
        private UniqueMap<String, AudioObject> m_AudioObjects;


        private UniqueMap<Int64, byte[]> m_Frames;
        public BindingList<AnimationClass> Classes { get { return m_Classes.GetValueBinding(); } }
        public BindingList<AudioObject> AudioObjects { get { return m_AudioObjects.GetValueBinding(); } }
        public String Directory { get; private set; }
        public LibraryMode SourceMode { get; private set; }
        public Int32 AnimationSetCount { get; private set; }
        public Int32 FrameCount { get; private set; }
        public Boolean IsReadonly { get; private set; }

        private AnimationLibrary()
        {
            m_Frames = new UniqueMap<long, byte[]>();
            m_Classes = new UniqueMap<string, AnimationClass>();
            m_AudioObjects = new UniqueMap<string, AudioObject>();
            SourceMode = LibraryMode.Filesystem;
            IsReadonly = true;
        }

        /// <summary>
        /// Registers the library in a static list <see cref="AnimationLibrary.Libraries"/>. This will prevent
        /// the library from being GCed. You are responsible to call <see cref="UnregisterAndAllowGC"/> when
        /// you don't need this library anymore. You only need to register a library, when it should expose
        /// shared animations.
        /// </summary>
        internal void RegisterAndPreventGC()
        {
            if (m_Libraries.Contains(this))
                throw new InvalidOperationException("This library is already registered!");

            m_Libraries.Add(this);
        }

        internal void UnregisterAndAllowGC()
        {
            m_Libraries.Remove(this);
        }

        internal void Modify()
        {
            if (IsReadonly)
                throw new InvalidOperationException("Library is read-only.");
        }

        public void Save()
        {
            Modify();

            // serialize library
            Stream target = File.OpenWrite(Directory + "\\AnimationLibrary.ali.tmp");
            BinaryWriter writer = new BinaryWriter(target);

            target.SetLength(0);

            using (target)
            {
                writer.Write((Byte)1); // library type ID
                writer.Write((UInt16)0x1000); // library version

                // serialize audio objects
                writer.Write((Int32)m_AudioObjects.Count);

                foreach (var audio in m_AudioObjects.Values)
                {
                    audio.Save(writer);
                }

                // serialize classes
                writer.Write((Int32)m_Classes.Count);

                foreach (var animClass in m_Classes.Values)
                {
                    animClass.Save(writer);
                }
            }

            File.Delete(Directory + "\\AnimationLibrary.ali");
            File.Move(Directory + "\\AnimationLibrary.ali.tmp", Directory + "\\AnimationLibrary.ali");

            // collect frames
            UniqueMap<long, byte[]> newFrames = new UniqueMap<long, byte[]>();

            foreach (AudioObject audio in AudioObjects)
            {
                audio.Load();

                newFrames.Add(audio.Checksum, audio.m_AudioBytes);
            }

            m_Frames = newFrames;

            // serialize frames
            File.Delete(Directory + "\\AnimationLibrary.gfx.tmp");

            target = File.OpenWrite(Directory + "\\AnimationLibrary.gfx.tmp");

            using (target)
            {
                writer = new BinaryWriter(target);

                writer.Write((Int32)m_Frames.Count);

                foreach (KeyValuePair<Int64, byte[]> entry in m_Frames)
                {
                    writer.Write((Int64)entry.Key);
                    writer.Write((Int32)entry.Value.Length);
                    writer.Write((byte[])entry.Value);
                }
            }

            File.Delete(Directory + "\\AnimationLibrary.gfx");
            File.Move(Directory + "\\AnimationLibrary.gfx.tmp", Directory + "\\AnimationLibrary.gfx");
        }


        internal static AnimationLibrary Load(BinaryReader inReader)
        {
            AnimationLibrary result = new AnimationLibrary();

            result.IsReadonly = false;

            try
            {
                if (inReader.ReadByte() != 1)
                    throw new InvalidDataException();

                UInt16 version = inReader.ReadUInt16();
                switch (version)
                {
                    case 0x1000:
                        {
                            for (int i = 0, count = inReader.ReadInt32(); i < count; i++)
                            {
                                AudioObject audio = AudioObject.Load(result, inReader);

                                result.m_AudioObjects.Add(audio.Name, audio);
                            }

                            SortedDictionary<long, int> frameIndices = new SortedDictionary<long, int>();

                            for (int i = 0, count = inReader.ReadInt32(); i < count; i++)
                            {
                                AnimationClass animClass = AnimationClass.Load(result, inReader);

                                result.m_Classes.Add(animClass.Name, animClass);

                                foreach (var set in animClass.Sets)
                                {
                                    foreach (var anim in set.Animations)
                                    {
                                        foreach (var frame in anim.Frames)
                                        {
                                            int index;

                                            if (frameIndices.TryGetValue(frame.Checksum, out index))
                                                frame.Index = index;
                                            else
                                            {
                                                frame.Index = result.FrameCount++;

                                                frameIndices.Add(frame.Checksum, frame.Index);
                                            }
                                        }
                                    }
                                    set.Index = result.AnimationSetCount;

                                    result.AnimationSetCount++;
                                }
                            }
                        } break;

                    default:
                        throw new InvalidDataException();
                }

                return result;
            }
            finally
            {
                result.IsReadonly = true;
            }
        }

        public static AnimationLibrary OpenOrCreate(String inRootDirectory)
        {
            AnimationLibrary result;

            if (!System.IO.File.Exists(inRootDirectory + "\\AnimationLibrary.ali"))
                result = Create(inRootDirectory);
            else
                result = OpenFromDirectory(inRootDirectory);

            result.IsReadonly = false;

            return result;
        }

        public static AnimationLibrary Create(String inRootDirectory)
        {
            AnimationLibrary result = new AnimationLibrary();

            if (System.IO.File.Exists(inRootDirectory + "\\AnimationLibrary.ali"))
                throw new ArgumentException("The given directory \"" + inRootDirectory + "\" does already contain an animation library!");

            System.IO.Directory.CreateDirectory(inRootDirectory);

            result.Directory = System.IO.Path.GetFullPath(inRootDirectory);
            result.IsReadonly = false;

            return result;
        }


        public static AnimationLibrary OpenFromDirectory(String inRootDirectory)
        {
            AnimationLibrary library = null;
            String fullPath = inRootDirectory;
            Stream source;

            // load library
            if (!System.IO.Directory.Exists(fullPath))
                throw new DirectoryNotFoundException("The given directory \"" + fullPath + "\" does not exist!");

            source = File.OpenRead(fullPath + "\\AnimationLibrary.ali");

            using (source)
            {
                library = OpenFromStream(source, fullPath);
            }

            return Instance = library;
        }

        private static AnimationLibrary OpenFromStream(Stream source, String fullPath)
        {
            AnimationLibrary library = Load(new BinaryReader(source));

            library.Directory = fullPath;

            // load shared frames
            library.m_Frames = new UniqueMap<long, byte[]>();
            
            if (File.Exists(fullPath + "\\AnimationLibrary.gfx"))
            {
                source = File.OpenRead(fullPath + "\\AnimationLibrary.gfx");
                BinaryReader reader = new BinaryReader(source);

                using (source)
                {
                    Int32 count = reader.ReadInt32();

                    for (int i = 0; i < count; i++)
                    {
                        Int64 checksum = reader.ReadInt64();

                        library.m_Frames.Add(checksum, reader.ReadBytes(reader.ReadInt32()));
                    }
                }
            }

            return library;
        }

        internal void ValidateName(String inName)
        {
            String validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_/";

            do
            {
                if (String.IsNullOrEmpty(inName))
                    break;

                if (inName.StartsWith("/") || inName.EndsWith("/"))
                    break;

                Boolean isValid = true;
                char prev = ' ';

                foreach (char c in inName)
                {
                    if (!validChars.Contains(c) || ((c == '/') && (prev == '/')))
                    {
                        isValid = false;

                        break;
                    }

                    prev = c;
                }

                if(isValid)
                    return;
            } while (false);

            throw new ArgumentException("Name \"" + inName + "\" is invalid.");
        }

        public void Rename(AnimationClass inClass, String inNewName)
        {
            Modify();

            ValidateName(inNewName);

            if (m_Classes.ContainsKey(inNewName))
                throw new ArgumentException("An animation class named \"" + inNewName + "\" does already exist!");

            int pos;

            if ((pos = m_Classes.Values.IndexOf(inClass)) < 0)
                throw new ApplicationException("Class does not belong to this set.");

            m_Classes.Remove(inClass.Name);
            inClass.Name = inNewName;
            m_Classes.Add(inClass.Name, inClass);
        }

        public AudioObject FindAudio(String inName)
        {
            try
            {
                return m_AudioObjects[inName];
            }
            catch (Exception e)
            {
                throw new ArgumentException("An audio object named \"" + inName + "\" does not exist!", e);
            }
        }

        public AnimationClass FindClass(String inName)
        {
            try
            {
                return m_Classes[inName];
            }
            catch (Exception e)
            {
                throw new ArgumentException("An animation class named \"" + inName + "\" does not exist!", e);
            }
        }

        public AnimationClass AddClass(String inName)
        {
            Modify();

            AnimationClass result = new AnimationClass(inName, this);

            ValidateName(inName);

            if (m_Classes.ContainsKey(inName))
                throw new ArgumentException("An animation class named \"" + inName + "\" does already exist!");

            System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Directory + "/" + inName));

            m_Classes.Add(inName, result);

            return result;
        }

        public void RemoveClass(AnimationClass inClass)
        {
            Modify();

            m_Classes.Remove(inClass.Name);
        }

        public AudioObject AddAudio(String inName, byte[] inWavBytes)
        {
            Modify();

            AudioObject result = new AudioObject(this, inWavBytes);

            ValidateName(inName);

            if (m_AudioObjects.ContainsKey(inName))
                throw new ArgumentException("An audio object named \"" + inName + "\" does already exist!");

            m_AudioObjects.Add(inName, result);
            result.Name = inName;

            return result;
        }

        public void RemoveAudio(AudioObject inAudio)
        {
            Modify();

            if (!m_AudioObjects.Remove(inAudio.Name))
                return;

            foreach (AnimationClass animClass in Classes)
            {
                foreach (AnimationSet set in animClass.Sets)
                {
                    foreach (Animation anim in set.Animations)
                    {
                        if (anim.Sound == inAudio)
                            anim.Sound = null;
                    }
                }
            }
        }

        internal byte[] LoadAudio(AudioObject inAudio)
        {
            byte[] result;

            if (!m_Frames.TryGetValue(inAudio.Checksum, out result))
                throw new FileNotFoundException("Failed to load audio object \"" + inAudio.Name + "\" (Checksum: " + inAudio.Checksum + ").");

            return result;
        }

        internal byte[] LoadFrame(AnimationFrame inFrame)
        {
            byte[] result;

            if (!m_Frames.TryGetValue(inFrame.Checksum, out result))
                throw new FileNotFoundException("Failed to load shared frame bitmap (Checksum: " + inFrame.Checksum + ") in animation \"" + inFrame.AnimationOrNull.Path + "\".");

            return result;
        }

    }
}
